/*
 * Copyright (c) 2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.libermundi.tapestry.elfinder.services.impl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.tapestry5.upload.services.UploadedFile;
import org.libermundi.tapestry.elfinder.exception.VolumeIOException;
import org.libermundi.tapestry.elfinder.services.FileType;
import org.libermundi.tapestry.elfinder.services.Volume;
import org.libermundi.tapestry.elfinder.services.VolumeOperation;
import org.libermundi.tapestry.elfinder.util.FilesUtils;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * @author Martin Papy
 *
 */
public abstract class AbstractVolume implements Volume {
	private String _volumeId;
	
	private String _volumeAlias;
	
	private File _basePath;
	
	private String _baseUrl;
	
	private long _maxFileSize;
	
	private Map<FileType, List<String>> _allowedExtensions = Maps.newHashMap();
	
	public AbstractVolume(File basePath, String baseUrl, long maxFileSize){
		_volumeId = UUID.randomUUID().toString();
		_basePath = basePath;
		_baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
		_volumeAlias = _basePath.getName();
		_maxFileSize = maxFileSize;
	}
	
	/**
	 * @param volumeId
	 * @param basePath
	 * @param baseUrl
	 * @param maxFileSize 
	 */
	public AbstractVolume(String volumeId, File basePath, String baseUrl, long maxFileSize) {
		this(basePath,baseUrl,maxFileSize);
		assert(!volumeId.matches(".*\\s+.*"));
		_volumeId = volumeId;
		_volumeAlias = basePath.getName();
	}

	public AbstractVolume(String volumeId,File basePath, String baseUrl, long maxFileSize, String volumeAlias){
		this(basePath,baseUrl,maxFileSize);
		assert(!volumeId.matches(".*\\s+.*"));
		_volumeId = volumeId;
		_volumeAlias=volumeAlias;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getBasePath()
	 */
	@Override
	public File getBasePath() {
		return _basePath;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getBaseUrl()
	 */
	@Override
	public String getBaseUrl() {
		return _baseUrl;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getClientUrl(java.io.File)
	 */
	@Override
	public String getClientUrl(File file) throws VolumeIOException {
		checkIfFileBelongsToVolume(file);
		if(file != null){
			return getBaseUrl() + "/" + getRelativeUrlPath(file);
		}
		return "#";
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getClientUrl(java.lang.String)
	 */
	@Override
	public String getClientUrl(String hash) throws VolumeIOException {
		File file = getFileFromHash(hash);
		return getClientUrl(file);
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getFileFromHash(java.lang.String)
	 */
	@Override
	public File getFileFromHash(String targetHash) throws VolumeIOException {
		return getFileFromHash(targetHash, getBasePath());
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getFileFromHash(java.lang.String, java.io.File)
	 */
	@Override
	public File getFileFromHash(String targetHash, File folder) throws VolumeIOException {
		checkIfFileBelongsToVolume(folder);
		if(isOperationAllowed(folder, VolumeOperation.READ)){
			if(hash(folder).equals(targetHash)) {
				return folder;
			}
			
			File[] children = folder.listFiles();
			if (children != null) {
				for (File child : children) {
					if (isAllowed(child) && isOperationAllowed(child, VolumeOperation.READ)) {
						if (hash(child).equals(targetHash)) {
							return child;
						}
						
						if(child.isDirectory()){
							File found = getFileFromHash(targetHash,child);
							if(found != null){
								return found;
							}
						}
					}
				}
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#hash(java.io.File)
	 */
	@Override
	public String hash(File path) {
		String hash = DigestUtils.md5Hex(path.getAbsolutePath());
		return getId() + "_" + hash;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getId()
	 */
	@Override
	public String getId() {
		return _volumeId;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getAlias()
	 */
	@Override
	public String getAlias(){
		return _volumeAlias;
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isAllowed(java.io.File)
	 */
	@Override
	public boolean isAllowed(File path) {
		return isAllowed(path, FileType.ANY);
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isAllowed(java.io.File, org.libermundi.tapestry.elfinder.services.FileType)
	 */
	@Override
	public boolean isAllowed(File path, FileType type) {
		if(path.isDirectory()){
			return Boolean.TRUE;
		}
		return isAllowed(path.getName(), type);
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isAllowed(java.lang.String)
	 */
	@Override
	public boolean isAllowed(String filename) {
		return isAllowed(filename, FileType.ANY);
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isAllowed(java.lang.String, org.libermundi.tapestry.elfinder.services.FileType)
	 */
	@Override
	public boolean isAllowed(String filename, FileType type) {
		List<String> extensions = _allowedExtensions.get(type); 
		if(extensions != null){
			return extensions.contains(FilenameUtils.getExtension(filename).toLowerCase());
		}
		return Boolean.FALSE;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#allowExtension(org.libermundi.tapestry.elfinder.services.FileType, java.lang.String[])
	 */
	@Override
	public void allowExtension(FileType type, String... extensions) {
		List<String> ext = _allowedExtensions.get(type);
		if(ext == null){
			ext = Lists.newArrayList();
			_allowedExtensions.put(type, ext);
		}
		if(extensions != null){
			for (int i = 0; i < extensions.length; i++) {
				ext.add(extensions[i].toLowerCase());
			}
		}
		
		if(type != FileType.ANY){
			allowExtension(FileType.ANY, extensions);
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getNewFile(java.lang.String, java.io.File)
	 */
	@Override
	public File getNewFile(String fileName, File existingDir) throws VolumeIOException {
		return getNewFile(fileName, existingDir, false);
	}

	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isValidFilename(java.lang.String)
	 */
	@Override
	public boolean isValidFilename(String fileName) {
		if (Strings.isNullOrEmpty(fileName)) {
			return Boolean.FALSE;
		}
		return fileName.matches("|^[^\\\\/\\<\\>:]+$|");
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isSpecialDir(java.io.File)
	 */
	@Override
	public boolean isSpecialDir(File file) {
		String fileName = file.getName();
		return (".".equals(fileName) || "..".equals(fileName));
	}

	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getRelativeUrlPath(java.io.File)
	 */
	@Override
	public String getRelativeUrlPath(File file) throws VolumeIOException{
		checkIfFileBelongsToVolume(file);
		String relativePath = file.getAbsolutePath().substring(getBasePath().getAbsolutePath().length());
		String relativeUrl = relativePath.replaceAll("\\\\", "/");
		if(relativeUrl.startsWith("/")){
			relativeUrl = relativeUrl.substring(1);
		}
		
		return relativeUrl;
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getPathSeparator()
	 */
	@Override
	public String getPathSeparator() {
		return System.getProperty("file.separator");
	}
	
		/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#isImage(java.io.File)
	 */
	@Override
	public boolean hasThumbnail(File file) {
		return (file.isFile()  && isAllowed(file.getName(), FileType.IMAGE));
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#createFolder(java.io.File)
	 */
	@Override
	public void createFolder(File newDir) throws VolumeIOException {
		checkIfFileBelongsToVolume(newDir);
		if(isOperationAllowed(newDir, VolumeOperation.CREATE)){
			if(!newDir.exists()){
				newDir.mkdirs();
			}
		} else {
			throw new VolumeIOException("Unable to Create folder. Operation Forbiden");
		}		
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#createFile(java.io.File)
	 */
	@Override
	public void createFile(File newFile, ByteArrayOutputStream os) throws VolumeIOException {
		checkIfFileBelongsToVolume(newFile);
		if(isOperationAllowed(newFile, VolumeOperation.CREATE)){
			if (os == null) {
				os = new ByteArrayOutputStream();
			}
			try {
				if (!newFile.createNewFile()) {
					throw new VolumeIOException("Unable to create file");
				}
				try {
					FileOutputStream fs = new FileOutputStream(newFile);
					fs.write(os.toByteArray());
					fs.flush();
					fs.close();
				} catch (Exception e) {
					newFile.delete();
					throw new VolumeIOException("Unable to write file",e);
				}
			} catch (Exception e) {
				throw new VolumeIOException("Unable to create file",e);
			}
		} else {
			throw new VolumeIOException("Unable to Create file. Operation Forbiden");
		}
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#rename(java.io.File, java.io.File)
	 */
	@Override
	public void rename(File fileTarget, File futureFile) throws VolumeIOException {
		checkIfFileBelongsToVolume(fileTarget);
		checkIfFileBelongsToVolume(futureFile);
		if(isOperationAllowed(fileTarget, VolumeOperation.UPDATE)){
			if(!fileTarget.renameTo(futureFile)){
				throw new VolumeIOException("Could not rename File : " + fileTarget.getName() + " to : " + futureFile.getName());
			}
		} else {
			throw new VolumeIOException("Unable to Rename file. Operation Forbiden");
		}
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#copy(java.io.File, java.io.File)
	 */
	@Override
	public void copy(File fileTarget, File futureFile) throws VolumeIOException {
		checkIfFileBelongsToVolume(fileTarget);
		checkIfFileBelongsToVolume(futureFile);
		if(isOperationAllowed(fileTarget, VolumeOperation.READ) && isOperationAllowed(futureFile, VolumeOperation.CREATE)){
			try {
				if (fileTarget.isDirectory()) {
					FileUtils.copyDirectory(fileTarget, futureFile);
				} else {
					FileUtils.copyFile(fileTarget, futureFile);
				}
			} catch (IOException e) {
				throw new VolumeIOException("Unable to copy file from " + fileTarget.getPath() + " to " + futureFile.getPath());
			}
		} else {
			throw new VolumeIOException("Unable to Copy file. Operation Forbiden");
		}
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#delete(java.io.File)
	 */
	@Override
	public void delete(File path) throws VolumeIOException {
		checkIfFileBelongsToVolume(path);
		if(isOperationAllowed(path, VolumeOperation.DELETE)){
			FileUtils.deleteQuietly(path);
			return;
		}
		throw new VolumeIOException("Unable to Delete file. Operation Forbiden");
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#save(org.apache.tapestry5.upload.services.UploadedFile, java.io.File, org.libermundi.tapestry.elfinder.services.FileType)
	 */
	@Override
	public File save(UploadedFile value, File workingDirectory) throws VolumeIOException {
		return save(value,workingDirectory,FileType.ANY);
	}

	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#save(org.apache.tapestry5.upload.services.UploadedFile, java.io.File, org.libermundi.tapestry.elfinder.services.FileType)
	 */
	@Override
	public File save(UploadedFile uploadedFile, File workingDirectory, FileType fileType) throws VolumeIOException {
		checkIfFileBelongsToVolume(workingDirectory);

		String targetFileName;
		
		if(!isAllowed(uploadedFile.getFileName(), fileType)){
			throw new VolumeIOException("You are not allowed to Upload files with extension : " + FilenameUtils.getExtension(uploadedFile.getFileName()));
		}
		
		if(!isValidFilename(uploadedFile.getFileName())){
			throw new VolumeIOException("Invalid name");
		}

		targetFileName = uploadedFile.getFileName();
		File targetFile = getNewFile(targetFileName, workingDirectory, true);
		
		uploadedFile.write(targetFile);

		return targetFile;			
		
	}

	/*
	 * (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getNewFile(java.lang.String, java.io.File, boolean)
	 */
	@Override
	public File getNewFile(String targetFileName, File workingDirectory, boolean makeUnique) throws VolumeIOException {
		checkIfFileBelongsToVolume(workingDirectory);
		String origTargetFileName = FilesUtils.cleanFileName(targetFileName,false);
		if (!isValidFilename(origTargetFileName)) {
			throw new VolumeIOException("Invalid name");
		}

		File newFile = new File(workingDirectory, origTargetFileName);
		int i = 0;
		if(makeUnique){
			while(newFile.exists()){
				String uniqueFileName = FilenameUtils.getBaseName(origTargetFileName) + "_" + i + "." + FilenameUtils.getExtension(origTargetFileName);
				newFile = new File(workingDirectory,uniqueFileName);
				i++;
			}
		} else if (newFile.exists()) {
			throw new VolumeIOException("File or folder with the same name already exists");
		}

		return newFile;
	}

	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getTotalSize()
	 */
	@Override
	public long getTotalSize() {
		return FileUtils.sizeOfDirectory(getBasePath());
	}
	
	/* (non-Javadoc)
	 * @see org.libermundi.tapestry.elfinder.services.Volume#getMaxUploadSize()
	 */
	@Override
	public long getMaxUploadSize() {
		return _maxFileSize;
	}	

	/**
	 * @param file
	 * @throws VolumeIOException 
	 */
	protected void checkIfFileBelongsToVolume(File file) throws VolumeIOException {
		if(file != null) {
			if(file.equals(getBasePath())){
				return;
			}
			File parent = file.getParentFile();
			while(parent != null) {
			    if(parent.equals(getBasePath())){
			    	return;
			    }
			    parent = parent.getParentFile();
			}
			throw new VolumeIOException("Attempt to manipulate a File that is not part of the current Volume !");
		}
	}	
}
